Status: HIDUP TAPI BERHENTI MENGHASILKAN. Infra, kode, dan DB sehat. Yang menghalangi
satu kecocokan jam antara crontab dan gate di skrip. Sejak 3 November 2026 tidak ada
artikel baru; per 11 November = 8 hari kosong.
| Hal | Nilai |
|---|---|
| Aplikasi Zadv | zpromo.zomet.my.id โ /api/health โ 200 {"ok":true,"service":"zadv"} |
| Crontab | 0 0 * * * โ docker exec af7wit npx tsx scripts/artikel-harian.ts |
| Gate jam di skrip | artikel_jam default 07:00 |
| Tabel Pengaturan | 0 baris (semua setelan memakai default kode) |
| Artikel published | 66 ยท terakhir 3 Nov 07:25 |
| scheduled menumpuk | 12 (19 Okt: 3 ยท 4 Nov: 4 ยท 6 Nov: 5) |
| draft menggantung | 3 (15 Okt, tanpa scheduledFor) |
| Commit artikel di zomet-main | terakhir 3 Nov |
crontab jalankan 00:00 WIB gate if (nowWib.getUTCHours() < 7) return "SKIP"
crontab jalankan 00:00 WIB
gate if (nowWib.getUTCHours() < 7) return "SKIP"
Jam 00 memang lebih kecil dari 07 โ **setiap malam cron menolak dirinya sendiri sebelum menyentuh apa pun.** Log membuktikan, delapan baris terakhir berturut-turut:
Jam 00 memang lebih kecil dari 07 โ **setiap malam cron menolak dirinya sendiri sebelum
menyentuh apa pun.** Log membuktikan, delapan baris terakhir berturut-turut:
SKIP: belum waktunya (setting=07:00, now WIB=00:00)
SKIP: belum waktunya (setting=07:00, now WIB=00:00)
Jam terbit terakhir `07:25`, bukan `07:00` tepat โ itu tanda artikel terakhir lahir dari **trigger manual**, bukan cron. Jadi sejak cron dipindah ke VPS, jamnya tidak pernah cocok. Bukan salah satu pihak saja: crontab menyala tengah malam, skrip menunggu pukul tujuh, dan karena keduanya disimpan di tempat berbeda (crontab sistem vs `Pengaturan` DB), tidak ada yang memberi tahu saat keduanya berselisih. ## Yang SUDAH benar (jangan diutak-atik) Diperiksa langsung di container yang berjalan, bukan dari repo:
Jam terbit terakhir 07:25, bukan 07:00 tepat โ itu tanda artikel terakhir lahir dari
trigger manual, bukan cron. Jadi sejak cron dipindah ke VPS, jamnya tidak pernah cocok.
Bukan salah satu pihak saja: crontab menyala tengah malam, skrip menunggu pukul tujuh, dan
karena keduanya disimpan di tempat berbeda (crontab sistem vs Pengaturan DB), tidak ada
yang memberi tahu saat keduanya berselisih.
Diperiksa langsung di container yang berjalan, bukan dari repo:
scheduledFor: { lte: todayEnd } // catch-up backlog hari terlewat
orderBy: { scheduledFor: 'asc' } // slot terlama dulu
const doneApps = new Set<number>() // maks 1 artikel per app per run
guard per slot (appId + scheduledFor) // tidak terbit dobelscheduledFor: { lte: todayEnd } // catch-up backlog hari terlewat
orderBy: { scheduledFor: 'asc' } // slot terlama dulu
const doneApps = new Set<number>() // maks 1 artikel per app per run
guard per slot (appId + scheduledFor) // tidak terbit dobel
Keempatnya sudah terpasang. Artinya 12 `scheduled` yang menumpuk **akan mengalir sendiri** begitu gate-nya lolos โ sekitar 5 hari pada 5 app, tidak perlu dipublish manual. Generate menarget `today + 3`; publish mengambil `scheduledFor <= akhir hari ini`. Selisih itu memang sengaja: artikel disiapkan tiga hari lebih awal, lalu terbit sesuai jadwalnya. ## Dua konsep perbaikan ### A. Selaraskan crontab (satu perintah, tanpa deploy)
Keempatnya sudah terpasang. Artinya 12 scheduled yang menumpuk akan mengalir sendiri
begitu gate-nya lolos โ sekitar 5 hari pada 5 app, tidak perlu dipublish manual.
Generate menarget today + 3; publish mengambil scheduledFor <= akhir hari ini. Selisih
itu memang sengaja: artikel disiapkan tiga hari lebih awal, lalu terbit sesuai jadwalnya.
0 0 * * * โ 0 7 * * *
0 0 * * * โ 0 7 * * *
Cocok dengan default `artikel_jam = 07:00`. Sekali jalan, langsung hidup. **Kelemahan:** kalau jam diubah dari dashboard jadi `08:00`, sistem mati lagi dengan cara yang persis sama โ dan tidak ada yang memberi tahu sampai artikel berhenti muncul. Ini menambal gejalanya, bukan sebabnya. ### B. Gate mengikuti setelan, bukan menunggu (REKOMENDASI)
Cocok dengan default artikel_jam = 07:00. Sekali jalan, langsung hidup.
Kelemahan: kalau jam diubah dari dashboard jadi 08:00, sistem mati lagi dengan cara
yang persis sama โ dan tidak ada yang memberi tahu sampai artikel berhenti muncul.
Ini menambal gejalanya, bukan sebabnya.
crontab 0 * * * * (mengetuk tiap jam) gate nowWib.getUTCHours() !== settingHour (bukan <)
crontab 0 * * * * (mengetuk tiap jam)
gate nowWib.getUTCHours() !== settingHour (bukan <)
Cron mengetuk tiap jam; hanya jam yang sama dengan setelan dashboard yang bekerja. Ubah jam dari dashboard sesukanya, tidak ada yang perlu disentuh lagi. Kenapa aman: ketukan yang tidak kena jam langsung `return` sebelum Gemini dipanggil, dan guard idempoten (`doneApps`, dedup per slot) membuat ketukan berulang murah โ tidak ada artikel dobel, tidak ada biaya API. **Konsekuensi yang harus diterima:** cron berdetak 24ร sehari. Itu memang tujuannya โ satu sumber kebenaran (dashboard) dan cron hanya pemicu bodoh yang tidak perlu tahu jam. ## Tindakan lain yang perlu, terlepas dari A/B ### 1. Lima app dicentang aktif tapi tidak punya URL
Cron mengetuk tiap jam; hanya jam yang sama dengan setelan dashboard yang bekerja.
Ubah jam dari dashboard sesukanya, tidak ada yang perlu disentuh lagi.
Kenapa aman: ketukan yang tidak kena jam langsung return sebelum Gemini dipanggil, dan
guard idempoten (doneApps, dedup per slot) membuat ketukan berulang murah โ tidak ada
artikel dobel, tidak ada biaya API.
Konsekuensi yang harus diterima: cron berdetak 24ร sehari. Itu memang tujuannya โ
satu sumber kebenaran (dashboard) dan cron hanya pemicu bodoh yang tidak perlu tahu jam.
KOSONG : ZGold ยท ZBengkel ยท ZBilliar ยท Z-Resto ยท Z-Absen (artikelAktif = true) JALAN : Z1 Pos ยท ZXGym ยท Z-Rooms ยท ZX Parenting ยท ZX Pajak
KOSONG : ZGold ยท ZBengkel ยท ZBilliar ยท Z-Resto ยท Z-Absen (artikelAktif = true)
JALAN : Z1 Pos ยท ZXGym ยท Z-Rooms ยท ZX Parenting ยท ZX Pajak
Query generate menyaring `url: { not: null }`, jadi kelimanya dilewati **diam-diam**.
Dashboard menampilkan 10 app aktif, yang benar-benar bekerja 5. Isi URL-nya, atau hapus
centangnya supaya setelan tidak menipu.
### 2. Tiga draft 15 Oktober menggantung
Tanpa `scheduledFor`, sistem tidak akan pernah menyentuhnya. Buang, atau jadwalkan manual.
### 3. Sebaran artikel tidak rata
Query generate menyaring url: { not: null }, jadi kelimanya dilewati diam-diam.
Dashboard menampilkan 10 app aktif, yang benar-benar bekerja 5. Isi URL-nya, atau hapus
centangnya supaya setelan tidak menipu.
Tanpa scheduledFor, sistem tidak akan pernah menyentuhnya. Buang, atau jadwalkan manual.
Z1 Pos 15 ยท ZXGym 15 ยท Z-Rooms 15 ยท ZX Parenting 9 ยท ZX Pajak 7 ZGold 1 ยท ZBengkel 1 ยท ZBilliar 1 ยท Z-Resto 1 ยท Z-Absen 1
Z1 Pos 15 ยท ZXGym 15 ยท Z-Rooms 15 ยท ZX Parenting 9 ยท ZX Pajak 7
ZGold 1 ยท ZBengkel 1 ยท ZBilliar 1 ยท Z-Resto 1 ยท Z-Absen 1
Angka 1 itu sisa seed awal, bukan hasil kerja sistem. ## Cara memverifikasi (jangan percaya satu sumber) Tiga sumber harus sepakat โ halaman artikel yang tetap tampil **bukan** bukti cron jalan, karena UI menyajikan data lama:
Angka 1 itu sisa seed awal, bukan hasil kerja sistem.
Tiga sumber harus sepakat โ halaman artikel yang tetap tampil bukan bukti cron jalan,
karena UI menyajikan data lama:
1. DB Zadv
SELECT status, count(*) FROM "ArtikelDraft" GROUP BY 1;
SELECT max("publishedAt") FROM "ArtikelDraft";
SELECT count(*) FILTER (WHERE status='scheduled' AND "scheduledFor" < current_date)
FROM "ArtikelDraft"; -- backlog yang menumpuk
2. Log cron
grep SELESAI /tmp/artikel-harian.log | tail -3
โ "SELESAI: published=N, generated=M, failed=0" adalah satu-satunya tanda cron benar
bekerja. Tanpa baris ini, jangan sebut cron hidup.
3. Repo zomet-main
git log origin/main --oneline | grep 'artikel scheduled'1. DB Zadv
SELECT status, count(*) FROM "ArtikelDraft" GROUP BY 1;
SELECT max("publishedAt") FROM "ArtikelDraft";
SELECT count(*) FILTER (WHERE status='scheduled' AND "scheduledFor" < current_date)
FROM "ArtikelDraft"; -- backlog yang menumpuk
2. Log cron
grep SELESAI /tmp/artikel-harian.log | tail -3
โ "SELESAI: published=N, generated=M, failed=0" adalah satu-satunya tanda cron benar
bekerja. Tanpa baris ini, jangan sebut cron hidup.
3. Repo zomet-main
git log origin/main --oneline | grep 'artikel scheduled'
## Pitfall yang sudah pernah menggigit
| Gejala | Sebab | Catatan |
|---|---|---|
| Log penuh `permission denied /var/run/docker.sock`, tanpa satu pun `SELESAI` | user `djvpri` dulu tidak ada di grup `docker` | **sudah beres** โ `groups djvpri` kini memuat `docker`. Baris itu sisa riwayat, bukan masalah sekarang |
| Log penuh `SKIP: belum waktunya` | crontab 00:00 vs gate 07:00 | **belum beres** โ inilah yang dibahas |
| Artikel terbit tapi tidak tampil di homepage konsumen | section memakai `{artikel.length > 0 && ...}`, dan fetch `!res.ok โ []` | 503 Zadv menghapus section tanpa error; cek `/api/artikel/publik?appId=N` dulu |
| `Bad credentials` saat push | token `ZOMET_PUSH_TOKEN` invalid atau tidak punya akses `zomet-main` | token valid tapi kurang izin โ 403, bukan 401 |
| `422 [SHA]: Required` dari Gitea | berkas BARU harus `POST`; `PUT` tanpa sha ditolak | atau token kurang scope tulis |
## Urutan pengerjaan yang saya sarankan
| Gejala | Sebab | Catatan |
|---|---|---|
| Log penuh permission denied /var/run/docker.sock, tanpa satu pun SELESAI | user djvpri dulu tidak ada di grup docker | sudah beres โ groups djvpri kini memuat docker. Baris itu sisa riwayat, bukan masalah sekarang |
| Log penuh SKIP: belum waktunya | crontab 00:00 vs gate 07:00 | belum beres โ inilah yang dibahas |
| Artikel terbit tapi tidak tampil di homepage konsumen | section memakai {artikel.length > 0 && ...}, dan fetch !res.ok โ [] | 503 Zadv menghapus section tanpa error; cek /api/artikel/publik?appId=N dulu |
| Bad credentials saat push | token ZOMET_PUSH_TOKEN invalid atau tidak punya akses zomet-main | token valid tapi kurang izin โ 403, bukan 401 |
| 422 [SHA]: Required dari Gitea | berkas BARU harus POST; PUT tanpa sha ditolak | atau token kurang scope tulis |
1. Terapkan konsep B (crontab tiap jam + gate !==) 2. Isi URL lima app kosong, atau hapus centangnya 3. Buang tiga draft 15 Oktober 4. Panggil trigger sekali โ buktikan 12 scheduled mulai mengalir (maks 5/hari) 5. Seminggu kemudian: pastikan max(publishedAt) bergerak tanpa disentuh
1. Terapkan konsep B (crontab tiap jam + gate !==)
2. Isi URL lima app kosong, atau hapus centangnya
3. Buang tiga draft 15 Oktober
4. Panggil trigger sekali โ buktikan 12 scheduled mulai mengalir (maks 5/hari)
5. Seminggu kemudian: pastikan max(publishedAt) bergerak tanpa disentuh
Langkah 4 penting: tanpa itu, perbaikan hanya terlihat sebagai "tidak ada error", bukan
sebagai artikel yang benar-benar terbit.
---
## Status terkini (11 Nov 2026) โ sudah diperbaiki
Konsep **B** diterapkan dan terbukti bekerja: crontab jadi `0 * * * *`, gate di skrip
memakai `!==`, dan cron pukul 12:06 menghasilkan `SELESAI: published=5, generated=5, failed=0.`
DB `published` 66 โ 71, `max("publishedAt")` bergerak ke 11 Nov, dan 5 commit
`feat: artikel scheduled - ...` masuk ke `zomet-main`.
Rincian lengkap beserta bukti ada di bagian **9. HASIL**. Sisa pekerjaan: lima app tanpa URL,
tiga draft 15 Oktober, dan alarm yang belum ada.
Langkah 4 penting: tanpa itu, perbaikan hanya terlihat sebagai "tidak ada error", bukan
sebagai artikel yang benar-benar terbit.
---
Konsep B diterapkan dan terbukti bekerja: crontab jadi 0 * * * *, gate di skrip
memakai !==, dan cron pukul 12:06 menghasilkan SELESAI: published=5, generated=5, failed=0.
DB published 66 โ 71, max("publishedAt") bergerak ke 11 Nov, dan 5 commit
feat: artikel scheduled - ... masuk ke zomet-main.
Rincian lengkap beserta bukti ada di bagian 9. HASIL. Sisa pekerjaan: lima app tanpa URL,
tiga draft 15 Oktober, dan alarm yang belum ada.
Dokumentasi disusun dari pemeriksaan langsung pada **crontab user djvpri, container Zadv, dan DB produksi โ bukan dari membaca repo. Semua angka diambil 12 November 2026**.
| Hal | Nilai | Sumber |
|---|---|---|
| Kesehatan Zadv | 200 {"ok":true} | /api/health |
| Crontab artikel | 0 * * * * (user djvpri) | crontab -u djvpri -l |
| Gate jam skrip | jamWib >= settingHour + berhenti bila hari ini sudah terisi | scripts/artikel-harian.ts |
| Artikel published | 75 | ArtikelDraft |
| Terbit terakhir | 10 Nov 2026 23:15 | max("publishedAt") |
| scheduled menunggu | 15 (13โ16 Nov) | per tanggal di bawah |
| Terlewat (jadwal lampau) | 0 | dijaga batasAkhirHariWib() |
| Pool foto | 62 | ArtikelFoto |
| App dipromosikan | 8 dari 14 (6 nonaktif) | PromoApp.artikelAktif |
13 Nov scheduled 5 14 Nov scheduled 8 15 Nov scheduled 1 16 Nov scheduled 1
13 Nov scheduled 5
14 Nov scheduled 8
15 Nov scheduled 1
16 Nov scheduled 1
Catatan: 8 artikel pada 14 Nov adalah **8 app berbeda**, bukan 8 artikel untuk satu app. Batas harian berlaku per app. ## Yang berubah sejak 11 Nov Sistem ini sudah dua kali berhenti tanpa suara. Yang dijaga sekarang bukan hanya "apakah jalan", tetapi tiga hal berbeda: **apakah jalan**, **apakah hasilnya berguna**, dan **apakah kegagalan meninggalkan jejak**.
Catatan: 8 artikel pada 14 Nov adalah 8 app berbeda, bukan 8 artikel untuk satu app. Batas harian berlaku per app.
Sistem ini sudah dua kali berhenti tanpa suara. Yang dijaga sekarang bukan hanya "apakah jalan", tetapi tiga hal berbeda: apakah jalan, apakah hasilnya berguna, dan apakah kegagalan meninggalkan jejak.
Empat bug ditemukan dan diperbaiki 12 November 2026, semuanya terpasang di container yang berjalan.
// SEBELUM โ batas "hari ini" dihitung zona proses (UTC)
const todayEnd = new Date(todayStart.getTime() + 24 * 3600 * 1000)
scheduledFor: { lte: todayEnd }
// SESUDAH โ batas akhir hari WIB
scheduledFor: { lt: batasAkhirHariWib() }// SEBELUM โ batas "hari ini" dihitung zona proses (UTC)
const todayEnd = new Date(todayStart.getTime() + 24 * 3600 * 1000)
scheduledFor: { lte: todayEnd }
// SESUDAH โ batas akhir hari WIB
scheduledFor: { lt: batasAkhirHariWib() }
Akibat nyata: tiga draft berjadwal **18 Okt 17:00 WIB** (= 10:00 UTC) dianggap *masa depan* dan macet **25 hari**. Komentar di kode bahkan menjanjikan "backlog hari yang terlewat ikut dikerjakan" โ janji itu tidak ditepati kodenya. ## 2. Backlog menumpuk lalu terbit sekaligus
Akibat nyata: tiga draft berjadwal 18 Okt 17:00 WIB (= 10:00 UTC) dianggap *masa depan* dan macet 25 hari. Komentar di kode bahkan menjanjikan "backlog hari yang terlewat ikut dikerjakan" โ janji itu tidak ditepati kodenya.
13 Nov : 5 artikel 14 Nov : 8 artikel โ terlihat seperti spam di /artikel
13 Nov : 5 artikel
14 Nov : 8 artikel โ terlihat seperti spam di /artikel
Batas lama hanya berlaku **per run**, bukan per hari. Sekarang dihitung dari artikel yang sudah terbit hari ini per app; parameter `tanpaBatasHarian` disediakan untuk kuras manual dari dashboard. ## 3. Satu ketukan gagal menghapus seluruh hari
Batas lama hanya berlaku per run, bukan per hari. Sekarang dihitung dari artikel yang sudah terbit hari ini per app; parameter tanpaBatasHarian disediakan untuk kuras manual dari dashboard.
// SEBELUM โ sama persis, tanpa percobaan ulang
if (jamWib !== settingHour) return
// SESUDAH โ boleh mencoba lagi, berhenti hanya bila hari ini sudah terisi
const lewatJam = jamWib >= settingHour
const sudahHariIni = await prisma.artikelDraft.count({ where: { status: 'published', publishedAt: { gte: awalHariWib() } } })
if (sudahHariIni > 0) return// SEBELUM โ sama persis, tanpa percobaan ulang
if (jamWib !== settingHour) return
// SESUDAH โ boleh mencoba lagi, berhenti hanya bila hari ini sudah terisi
const lewatJam = jamWib >= settingHour
const sudahHariIni = await prisma.artikelDraft.count({ where: { status: 'published', publishedAt: { gte: awalHariWib() } } })
if (sudahHariIni > 0) return
Log lama sempat berbunyi `SKIP: jam WIB 08 bukan 07:00` empat kali sehari. Itu bukan perilaku yang salah โ tetapi artinya **ketukan 07:00 yang gagal tidak punya cadangan**. Gemini 503 atau VPS sibuk cukup untuk menghapus satu hari penuh. ## 4. Satu balasan Gemini cacat menghapus satu slot
Log lama sempat berbunyi SKIP: jam WIB 08 bukan 07:00 empat kali sehari. Itu bukan perilaku yang salah โ tetapi artinya ketukan 07:00 yang gagal tidak punya cadangan. Gemini 503 atau VPS sibuk cukup untuk menghapus satu hari penuh.
โ [AcuMedis] Bad escaped character in JSON at position 5283
โ [AcuMedis] Bad escaped character in JSON at position 5283
`parseGeminiJson` kini membuang escape tidak sah (`\\x`, `\\u` tanpa 4 digit), lalu **mencoba ulang sekali** dengan prompt yang sama. Pesan galatnya menyertakan cuplikan 120 karakter di sekitar posisi gagal โ dulu hanya nomor kolom tanpa isi, sehingga penyebabnya tidak bisa ditebak. ## Angka hasil
parseGeminiJson kini membuang escape tidak sah (\\x, \\u tanpa 4 digit), lalu mencoba ulang sekali dengan prompt yang sama. Pesan galatnya menyertakan cuplikan 120 karakter di sekitar posisi gagal โ dulu hanya nomor kolom tanpa isi, sehingga penyebabnya tidak bisa ditebak.
tsc --noEmit bersih test-artikel-fix.test.ts 4 lulus container Zadv batasAkhirHariWib โ lewatJam >= โ coba ulang sekali โ
tsc --noEmit bersih
test-artikel-fix.test.ts 4 lulus
container Zadv batasAkhirHariWib โ lewatJam >= โ coba ulang sekali โ
--- ## Bagian yang DIPERTAHANKAN (masih berlaku) - **Structured data 8 halaman produk** โ penanda `Article`/`Product` masih terpasang di `zomet-main`. - **Kanomik 91 artikel + logo publisher** โ canonical dan `logo.png` sudah benar di produksi. - **Audit isi & putaran angle** โ `angleIdx` masih dipakai; dedup per slot menjaga artikel kembar. - **Cara verifikasi** โ lihat bagian berikut.
---
Article/Product masih terpasang di zomet-main.logo.png sudah benar di produksi.angleIdx masih dipakai; dedup per slot menjaga artikel kembar.0 * * * * artikel-harian.ts โ mengetuk tiap jam, gate di dalam skrip 0 8 * * 1 fakta-produk.ts โ panen fakta produk, Senin 08:00 0 */3 * * * artikel-watchdog.sh โ alarm bila >30 jam tanpa artikel baru 0 */4 * * * disk-watchdog.sh
0 * * * * artikel-harian.ts โ mengetuk tiap jam, gate di dalam skrip
0 8 * * 1 fakta-produk.ts โ panen fakta produk, Senin 08:00
0 */3 * * * artikel-watchdog.sh โ alarm bila >30 jam tanpa artikel baru
0 */4 * * * disk-watchdog.sh
Cron artikel **tidak** di crontab `root` โ milik user `djvpri`, alasan historis (akses docker). Kalau `crontab -l` sebagai root terlihat bersih dari artikel, itu **bukan** tanda cron hilang. ## Kenapa mengetuk tiap jam, bukan `0 7 * * *` Mengetuk tiap jam membuat jam bisa diubah dari dashboard **tanpa menyentuh crontab**, dan memberi percobaan ulang otomatis bila jam pertama gagal. Biayanya nol: ketukan yang tidak lolos gate berhenti sebelum memanggil Gemini. ## Alur satu ketukan
Cron artikel tidak di crontab root โ milik user djvpri, alasan historis (akses docker). Kalau crontab -l sebagai root terlihat bersih dari artikel, itu bukan tanda cron hilang.
Mengetuk tiap jam membuat jam bisa diubah dari dashboard tanpa menyentuh crontab, dan memberi percobaan ulang otomatis bila jam pertama gagal. Biayanya nol: ketukan yang tidak lolos gate berhenti sebelum memanggil Gemini.
1. Ambil setelan (artikel_jam, tujuan_zomet, tujuan_app) 2. Gate: jamWib >= settingHour? hari ini sudah ada artikel? โ berhenti bila tidak lolos 3. publishScheduled() โ kuras yang scheduledFor < besok 00:00 WIB 4. generateSatuArtikel() per app, untuk hari+3 5. Push ke zomet-main via Gitea
1. Ambil setelan (artikel_jam, tujuan_zomet, tujuan_app)
2. Gate: jamWib >= settingHour? hari ini sudah ada artikel? โ berhenti bila tidak lolos
3. publishScheduled() โ kuras yang scheduledFor < besok 00:00 WIB
4. generateSatuArtikel() per app, untuk hari+3
5. Push ke zomet-main via Gitea
Langkah 3 mendahului 4 dengan sengaja: kalau Gemini mati, artikel yang **sudah ada** tetap terbit.
Langkah 3 mendahului 4 dengan sengaja: kalau Gemini mati, artikel yang sudah ada tetap terbit.
Delapan app dipromosikan; enam dimatikan (artikelAktif=false) tanpa dihapus โ datanya utuh untuk dihidupkan lagi.
AKTIF ZX Pajak Kendaraan Kalbar ยท ZX Parenting ยท ZXGym ยท Z1 Pos
Z-Rooms ยท ZX Carwash ยท YTVault ยท AcuMedis
MATI ZGold ยท ZBengkel ยท ZBilliar ยท Z-Resto ยท Z-Absen
(domain curl 000, /produk/<slug> 404)
BUKAN Zomet (artikelAktif=false, induk situs โ bukan produk)AKTIF ZX Pajak Kendaraan Kalbar ยท ZX Parenting ยท ZXGym ยท Z1 Pos
Z-Rooms ยท ZX Carwash ยท YTVault ยท AcuMedis
MATI ZGold ยท ZBengkel ยท ZBilliar ยท Z-Resto ยท Z-Absen
(domain curl 000, /produk/<slug> 404)
BUKAN Zomet (artikelAktif=false, induk situs โ bukan produk)
## Syarat app boleh dipromosikan
1. PromoApp.url hidup (curl 200) 2. PromoApp.halamanProduk ada dan tidak 404 3. artikelAktif = true
1. PromoApp.url hidup (curl 200)
2. PromoApp.halamanProduk ada dan tidak 404
3. artikelAktif = true
Kalau salah satu tidak terpenuhi, mesin akan menulis artikel yang menautkan ke halaman mati โ dan itu merusak kepercayaan pembaca sekaligus SEO. ## Cara menghidupkan app yang dimatikan
Kalau salah satu tidak terpenuhi, mesin akan menulis artikel yang menautkan ke halaman mati โ dan itu merusak kepercayaan pembaca sekaligus SEO.
1. Nyalakan domain app (DNS + deploy) 2. Pastikan /produk/<slug> hidup di zomet-main 3. PromoApp: isi url + halamanProduk, set artikelAktif = true
1. Nyalakan domain app (DNS + deploy)
2. Pastikan /produk/<slug> hidup di zomet-main
3. PromoApp: isi url + halamanProduk, set artikelAktif = true
| Gejala | Sebab | Status |
|---|---|---|
| Log penuh permission denied /var/run/docker.sock, tanpa satu pun SELESAI | user djvpri dulu tidak ada di grup docker; cron jalan sebagai djvpri | tetap waspada โ cron artikel memang milik user djvpri |
| SKIP: jam WIB 08 bukan 07:00 berulang | gate !== lama; ketukan lain tidak punya fungsi apa pun | diperbaiki 12 Nov โ >= + cek "hari ini sudah terisi" |
| Tiga draft macet 25 hari padahal cron jalan | batas todayEnd dihitung UTC | diperbaiki 12 Nov โ batasAkhirHariWib() |
| Artikel terbit 7โ8 sehari | batas hanya per run | diperbaiki 12 Nov โ batas per app per hari |
| Bad escaped character in JSON untuk satu app | balasan Gemini cacat, skrip menyerah | diperbaiki 12 Nov โ bersihkan + coba ulang sekali |
| Artikel kembar di slot yang sama | Math.random() untuk angle + tidak ada dedup | diperbaiki 11 Nov โ angleIdx putaran + guard slot |
| Halaman /produk/<slug> 404 padahal app aktif | lima app domain mati | diperbaiki 11 Nov โ artikelAktif=false |
| Deploy Zadv tetap in_progress di panel | bug pencatatan Coolify; log berakhir Rolling update completed | periksa container, jangan langsung deploy ulang |
Setiap gate waktu harus punya SATU sumber kebenaran, atau punya alarm saat dua sumber itu berselisih. Kegagalan yang keluar dengan exit 0 tidak akan pernah diberitakan oleh cron, sistem monitoring, maupun log โ yang terlihat hanya "tidak ada artikel", bukan "cron menolak jalan".
Setiap gate waktu harus punya SATU sumber kebenaran, atau
punya alarm saat dua sumber itu berselisih.
Kegagalan yang keluar dengan exit 0 tidak akan pernah diberitakan
oleh cron, sistem monitoring, maupun log โ yang terlihat hanya
"tidak ada artikel", bukan "cron menolak jalan".
Halaman artikel konsumen tetap tampil normal walau publisher mati berhari-hari (disajikan dari data lama). Jadi "halaman hidup" bukan bukti job jalan. Tiga sumber harus sepakat.
tail -30 /tmp/artikel-harian.log
tail -30 /tmp/artikel-harian.log
Yang dicari: baris `SELESAI: published=N`. Kalau hanya ada `SKIP`, gate menolak โ dan itu **bukan** tanda sehat. Perhatikan juga apakah `published=0` padahal `generated>0` (artikel dibuat tetapi tidak terbit). ## 2. DB Zadv
Yang dicari: baris SELESAI: published=N. Kalau hanya ada SKIP, gate menolak โ dan itu bukan tanda sehat. Perhatikan juga apakah published=0 padahal generated>0 (artikel dibuat tetapi tidak terbit).
select status, count(*) from "ArtikelDraft" group by status; select count(*) from "ArtikelDraft" where status='scheduled' and "scheduledFor" < now() - interval '1 day';
select status, count(*) from "ArtikelDraft" group by status;
select count(*) from "ArtikelDraft" where status='scheduled' and "scheduledFor" < now() - interval '1 day';
Angka kedua harus **0**. Kalau tidak nol, backlog macet lagi โ periksa `batasAkhirHariWib()` masih dipakai. ## 3. Isi yang benar-benar terbit
Angka kedua harus 0. Kalau tidak nol, backlog macet lagi โ periksa batasAkhirHariWib() masih dipakai.
select d.judul, d."scheduledFor", d."publishedAt", a.nama from "ArtikelDraft" d join "PromoApp" a on a.id=d."appId" where d.status='published' order by d."publishedAt" desc limit 5;
select d.judul, d."scheduledFor", d."publishedAt", a.nama
from "ArtikelDraft" d join "PromoApp" a on a.id=d."appId"
where d.status='published' order by d."publishedAt" desc limit 5;
Cek: `appId` menunjuk app yang **punya halaman produk**, dan `publishedAt` hari ini. ## 4. Watchdog `/root/artikel-watchdog.sh` (cron `0 */3 * * *`) mengirim WA bila **lebih dari 30 jam tidak ada artikel baru**, sekali per 12 jam. Alarm berbasis "tidak ada artikel baru", bukan "ada backlog" โ saat menguras 12 slot tertunda, alarm backlog akan berbunyi palsu berhari-hari.
Cek: appId menunjuk app yang punya halaman produk, dan publishedAt hari ini.
/root/artikel-watchdog.sh (cron 0 */3 * * *) mengirim WA bila lebih dari 30 jam tidak ada artikel baru, sekali per 12 jam. Alarm berbasis "tidak ada artikel baru", bukan "ada backlog" โ saat menguras 12 slot tertunda, alarm backlog akan berbunyi palsu berhari-hari.
crontab artikel 0 0 * * * โ 0 * * * * (user djvpri) gate jam getUTCHours() < settingHour โ !== settingHour 5 app domain mati artikelAktif = false (tidak dihapus) 3 app hidup ditambah ZX Carwash ยท YTVault ยท AcuMedis angle Math.random() โ putaran + kolom angleIdx tautan internal halamanProduk + UTM utm_source=artikel&utm_medium=blog panjang artikel ditolak bila < 550 kata sitemap + robots dibuat di zomet-main (98 URL) pool foto 26 โ 62 watchdog >30 jam tanpa artikel baru โ WA structured data Article/Product di 8 halaman produk
crontab artikel 0 0 * * * โ 0 * * * * (user djvpri)
gate jam getUTCHours() < settingHour โ !== settingHour
5 app domain mati artikelAktif = false (tidak dihapus)
3 app hidup ditambah ZX Carwash ยท YTVault ยท AcuMedis
angle Math.random() โ putaran + kolom angleIdx
tautan internal halamanProduk + UTM utm_source=artikel&utm_medium=blog
panjang artikel ditolak bila < 550 kata
sitemap + robots dibuat di zomet-main (98 URL)
pool foto 26 โ 62
watchdog >30 jam tanpa artikel baru โ WA
structured data Article/Product di 8 halaman produk
## 12 November 2026
batas backlog todayEnd (UTC) โ batasAkhirHariWib() batas harian per run โ per app per hari gate jam !== โ >= + berhenti bila hari ini terisi balasan Gemini cacat dibersihkan + coba ulang sekali + pesan bergalat cuplikan 3 draft duplikat dihapus (backup: /root/backup-artikeldraft-20261211-1234.sql)
batas backlog todayEnd (UTC) โ batasAkhirHariWib()
batas harian per run โ per app per hari
gate jam !== โ >= + berhenti bila hari ini terisi
balasan Gemini cacat dibersihkan + coba ulang sekali + pesan bergalat cuplikan
3 draft duplikat dihapus (backup: /root/backup-artikeldraft-20261211-1234.sql)
Yang **belum** dikerjakan dan masih terbuka:
Yang belum dikerjakan dan masih terbuka:
pool ArtikelIde masih kosong โ ide dari pertanyaan pelanggan WA belum terpakai WaRiwayat 0 baris batas "1 artikel per hari untuk SELURUH situs" (sekarang per app)
pool ArtikelIde masih kosong โ ide dari pertanyaan pelanggan WA belum terpakai
WaRiwayat 0 baris
batas "1 artikel per hari untuk SELURUH situs" (sekarang per app)